using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Reflection;

namespace Clifton.Tools.Data
{
	/// <summary>
	/// Used as a key to index the ColumnBinder instance.
	/// The key is a composite of the destination object and property.
	/// This composite key maps to one and only one ColumnBinder, because
	/// an object's property can map to only one column in a table.
	/// </summary>
	public struct PropertyBinding
	{
		public object destObject;
		public string destProperty;

		public PropertyBinding(object destObject, string destProperty)
		{
			this.destObject = destObject;
			this.destProperty = destProperty;
		}
	}

	public class TableBindHelperException : ApplicationException
	{
		public TableBindHelperException(string msg)
			: base(msg)
		{
		}
	}

	/// <summary>
	/// Container for mapping a column bound to an object-property.
	/// </summary>
	public class ColumnBinder
	{
		protected string columnName;
		protected object destObject;
		protected string destProperty;
		protected PropertyInfo propInfo;
		protected TableBindHelper tableBindHelper;

		public string ColumnName
		{
			get {return columnName;}
		}

		public object Object
		{
			get {return destObject;}
		}

		public PropertyInfo PropertyInfo
		{
			get { return propInfo; }
		}

		public ColumnBinder(TableBindHelper tableBindHelper, string columnName, object destObject, string destProperty)
		{
			this.tableBindHelper = tableBindHelper;
			this.columnName = columnName;
			this.destObject = destObject;
			this.destProperty = destProperty;
			propInfo = destObject.GetType().GetProperty(destProperty);
		}

		public void CreatePropertyWatcher()
		{
			Type eventType = destObject.GetType();
			EventInfo eventInfo = eventType.GetEvent(destProperty+"Changed");
			eventInfo.AddEventHandler(destObject, new EventHandler(OnDestinationChanged));
		}

		protected void OnDestinationChanged(object sender, EventArgs e)
		{
			// Reformat the event and pass it on to the handler that works with INotifyPropertyChanged objects.
			tableBindHelper.OnDestinationChanged(sender, new PropertyChangedEventArgs(destProperty));
		}
	}

	/// <summary>
	/// Provides binding between a DataTable and another object, without requiring the System.Windows.Forms namespace.
	/// Also implements a row cursor for record navigation and automatic object updating.
	/// </summary>
	public class TableBindHelper
	{
		protected DataTable table;
		protected int rowIdx;
		protected Dictionary<string, ColumnBinder> columnBinders;
		protected Dictionary<PropertyBinding, ColumnBinder> propertyBinders;

		/// <summary>
		/// Get/set the current row being bound.
		/// </summary>
		public int RowIndex
		{
			get { return RowIndex; }
			set { SetRowIndex(value); }
		}

		public TableBindHelper(DataTable table)
		{
			this.table = table;
			columnBinders = new Dictionary<string, ColumnBinder>();
			propertyBinders = new Dictionary<PropertyBinding, ColumnBinder>();
			table.ColumnChanged += new DataColumnChangeEventHandler(OnColumnChanged);
			table.RowDeleted += new DataRowChangeEventHandler(OnRowDeleted);
			table.RowChanged += new DataRowChangeEventHandler(OnRowChanged);
		}

		public void AddColumnBinder(string columnName, object dest, string propertyName)
		{
			AddColumnBinder(columnName, dest, propertyName, false);
		}

		/// <summary>
		/// Add a binding to a column of the table, whose target is the supplied object
		/// and property.
		/// </summary>
		public void AddColumnBinder(string columnName, object dest, string propertyName, bool useLegacyChangeEvent)
		{
			ColumnBinder cb = new ColumnBinder(this, columnName, dest, propertyName);
			columnBinders[columnName] = cb;
			PropertyBinding db = new PropertyBinding(dest, propertyName);
			propertyBinders[db]=cb;

			if ( (dest is INotifyPropertyChanged) && (!useLegacyChangeEvent) )
			{
				// Create a generic property watcher.
				CreatePropertyWatcher(dest, propertyName);
			}
			else
			{
				// Create the event sink in the container that knows about the 
				// the property name.
				cb.CreatePropertyWatcher();
			}

			if (rowIdx < table.Rows.Count)
			{
				object val = table.Rows[rowIdx][cb.ColumnName];
				UpdateTargetWithValue(cb, val);
			}
		}

		/// <summary>
		/// Adjusts the row index if the row index now is greater than the number
		/// of available rows.  Updates all destination objects to reflect a possible
		/// change in the data that the row index is indexing.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected void OnRowDeleted(object sender, DataRowChangeEventArgs e)
		{
			if (rowIdx >= table.Rows.Count)
			{
				// Can result in rowIdx set to -1.
				rowIdx = table.Rows.Count - 1;
			}

			if (rowIdx >= 0)
			{
				UpdateAllDestinationObjects();
			}
		}

		/// <summary>
		/// Updates the row index to the row being added.
		/// </summary>
		protected void OnRowChanged(object sender, DataRowChangeEventArgs e)
		{
			if (e.Action == DataRowAction.Add)
			{
				RowIndex = FindRow(e.Row);
			}
		}

		protected int FindRow(DataRow row)
		{
			int idx=0;
			bool found = false;

			foreach (DataRow tableRow in table.Rows)
			{
				if (tableRow == row)
				{
					++idx;
					found = true;
					break;
				}
			}

			if (!found)
			{
				throw new TableBindHelperException("The added row can not be found in the table.");
			}

			return idx;
		}

		/// <summary>
		/// Called when the column value is changed, so any bound object can have its
		/// property updated.
		/// </summary>
		protected void OnColumnChanged(object sender, DataColumnChangeEventArgs e)
		{
			ColumnBinder cb = null;
			bool ret = columnBinders.TryGetValue(e.Column.ColumnName, out cb);

			if (ret)
			{
				UpdateTargetWithValue(cb, e.ProposedValue);
			}
		}

		/// <summary>
		/// Updates the target with the column value, handling DBNull.Value.
		/// </summary>
		protected void UpdateTargetWithValue(ColumnBinder cb, object val)
		{
			if ( (val == null) || (val==DBNull.Value) )
			{
				// TODO: We need a more sophisticated way of:
				// 1: does the target handle null/DBNull.Value itself?
				// 2: specifying the default value associated with a property.
				val = String.Empty;
			}

			cb.PropertyInfo.SetValue(cb.Object, val, null);
		}

		/// <summary>
		/// Using the INotifyPropertyChanged interface, which requires the implementation
		/// of the PropertyChanged event, this method wires up a generic handler.
		/// </summary>
		protected void CreatePropertyWatcher(object dest, string propertyName)
		{
			// string eventName = propertyName + "Changed";
			Type eventType = dest.GetType();
			EventInfo eventInfo = eventType.GetEvent("PropertyChanged");
			eventInfo.AddEventHandler(dest, new PropertyChangedEventHandler(OnDestinationChanged));
		}

		/// <summary>
		/// Called when the bound object's value changes, so that the change can be
		/// reflected in the associated table's row and the bound column.
		/// </summary>
		public void OnDestinationChanged(object sender, PropertyChangedEventArgs e)
		{
			PropertyBinding db = new PropertyBinding(sender, e.PropertyName);
			ColumnBinder cb;
			bool ret = propertyBinders.TryGetValue(db, out cb);

			if (ret)
			{
				UpdateTablePropertyValue(cb);
			}
		}

		/// <summary>
		/// Updates the destiniation property value with the associated column
		/// in the current indexed row.
		/// </summary>
		protected void UpdateTablePropertyValue(ColumnBinder cb)
		{
			if (rowIdx < table.Rows.Count)
			{
				object val = cb.PropertyInfo.GetValue(cb.Object, null);

				if (val == null)
				{
					val = DBNull.Value;
				}

				table.Rows[rowIdx][cb.ColumnName] = val;
			}
		}

		/// <summary>
		/// Sets the current row index, updating all bound target objects and their properties.
		/// </summary>
		protected void SetRowIndex(int val)
		{
			if (val < 0)
			{
				throw new ArgumentOutOfRangeException("RowIndex cannot be < 0");
			}

			if (val >= table.Rows.Count)
			{
				throw new ArgumentOutOfRangeException("RowIndex cannot be >= table.Rows.Count");
			}

			if (rowIdx != val)
			{
				rowIdx = val;
				UpdateAllDestinationObjects();
			}
		}

		/// <summary>
		/// Updates all bound target object and their properties to the current row index.
		/// </summary>
		protected void UpdateAllDestinationObjects()
		{
			foreach (ColumnBinder cb in columnBinders.Values)
			{
				UpdateTargetWithValue(cb, table.Rows[rowIdx][cb.ColumnName]);
			}
		}
	}
}
